/**
 * FIFS - The Find It File System
 * Copyright (C) 2006 Chris Oklota (chris at oklota net)
 * 
 * This program is distributed under the terms of the GNU LGPL.
 * See the file http://www.gnu.org/licenses/lgpl.txt
 */
package fifs;


import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.sql.*;
import java.util.*;

import org.apache.commons.dbcp.BasicDataSource;
import org.apache.log4j.Logger;

import fuse.FuseException;
import fuse.FuseStat;

/**
 * This class implements the FifsDao interface by using a JDBC connection to the
 * filesystem relational database.
 */
public class FifsDaoImpl implements FifsDao {

	/** The logger for this class */
	public static final Logger log = Logger.getLogger(FifsDaoImpl.class);

	/** The connection pooled data source to the database */
	private BasicDataSource dataSource;


	/**
	 * Initialize the connection to the database using the configuration
	 * contained in the properties.
	 * 
	 * @param props The properties from the config file
	 * @throws FuseException
	 */
	public void init(Properties props) throws FuseException {
		dataSource = new BasicDataSource();
		dataSource.setDriverClassName(props.getProperty("jdbc.driver"));
		dataSource.setUrl(props.getProperty("jdbc.url"));
		dataSource.setUsername(props.getProperty("jdbc.username"));
		dataSource.setPassword(props.getProperty("jdbc.password"));

		try {
			Connection c = dataSource.getConnection();
		} catch (SQLException se) {
			throw new FuseException("failed to init jdbc", se);
		}
	}

	/**
	 * Get all tags in the datbase
	 * @return The set of tags in the database
	 */
	public Set<String> getAllTags() {
		//the simple query to get all the tags from the db
		String query = "SELECT name FROM tag ORDER BY name ASC";
		Set<String> tags = new HashSet<String>();

		//variables for the connection
		Connection conn = null;
		PreparedStatement stmt = null;
		ResultSet rs = null;

		//attempt to make a connection, and query the db for all the tags
		try {
			conn = dataSource.getConnection();
			stmt = conn.prepareStatement(query);
			rs = stmt.executeQuery();

			//loop through the results and add each tag to the set
			while (rs.next()) {
				String name = rs.getString("name");
				tags.add(name);
			}
		} catch (SQLException sqle) {
			log.error("Error in getAllTags():", sqle);
		} finally {
			try { rs.close(); } catch(Exception e) { }
			try { stmt.close(); } catch(Exception e) { }
			try { conn.close(); } catch(Exception e) { }
		}

		//give the set of tags back to the caller
		return tags;
	}

	/**
	 * Create the given tag in the database
	 * @param tag The tag to create
	 * @return true if the tag was created, false otherwise
	 */
	public boolean createTag(String tag) {
		//SQL query inserts the tag name into the database
		String query = "INSERT INTO tag (name) VALUES (\"" + tag + "\")";    	
		Connection conn = null;
		Statement stmt = null;
		int num_rows_inserted = 0;

		//attempt to make a connection, and insert the tag
		try {
			conn = dataSource.getConnection();
			stmt = conn.createStatement(java.sql.ResultSet.TYPE_FORWARD_ONLY, java.sql.ResultSet.CONCUR_UPDATABLE);

			//the real trickery here is using the return from executeUpdate to see how many rows were updates
			//since this is a single insert, we expect one row to be created
			num_rows_inserted = stmt.executeUpdate(query, Statement.RETURN_GENERATED_KEYS);
		} catch (SQLException sqle) {
			log.error("Error in createTag():", sqle);
		} finally {
			try { stmt.close(); } catch(Exception e) { }
			try { conn.close(); } catch(Exception e) { }
		}

		//return true if the insert worked, otherwise return false
		if(num_rows_inserted > 0) {
			return true;
		}
		return false;
	}

	/**
	 * Delete the given tag in the database. No need to clean up tag association entries, the
	 * Fifs code will update individual file tags as appropriate before a tag is removed.
	 * @param tag The tag to delete
	 * @return true if the tag was deleted, false otherwise
	 */
	public boolean deleteTag(String tag) {
		Connection conn = null;
		PreparedStatement stmt = null;
		int rowsUpdated = 0;

		try {
			conn = dataSource.getConnection();
			stmt = conn.prepareStatement("DELETE FROM tag WHERE tag.name=?");
			stmt.setString(1, tag);
			rowsUpdated = stmt.executeUpdate();    
		} catch (SQLException sqle) {
			log.error("Error deleting tag: ", sqle);
		} finally {
			try { stmt.close(); } catch(Exception e) { }
			try { conn.close(); } catch(Exception e) { }
		}

		if (rowsUpdated == 1) {
			return true;
		}
		return false;
	}

	/**
	 * Create the given file as an empty file in the database.
	 * @param file The file info used to create the new file.
	 * @return true if the file was created, false otherwise
	 */
	public boolean createFile(FifsFile file) {
		//to create the file, we need to insert into the attribute table first
		String name = file.getFilename();
		FuseStat a = file.getAttributes();
		String query = "INSERT INTO attribute (name, uid, gid, mode, ctime, mtime, atime, size, blocks) VALUES  (\"" + name + "\"," + 
			a.uid + "," + a.gid + "," + a.mode + "," + a.ctime + "," + 
			a.mtime + "," + a.atime + ", " + a.size + ", " + a.blocks + ")";

		Connection conn = null;
		Statement stmt = null;
		ResultSet rs = null;
		int new_fid = -1;

		//attempt to make a connection, and insert the file into the attribute table
		try {
			conn = dataSource.getConnection();
			stmt = conn.createStatement(java.sql.ResultSet.TYPE_FORWARD_ONLY, java.sql.ResultSet.CONCUR_UPDATABLE);

			//the real trickery here is using the return from executeUpdate to see how many rows were updates
			//since this is a single insert, we expect one row to be created
			stmt.executeUpdate(query, Statement.RETURN_GENERATED_KEYS);
			rs = stmt.getGeneratedKeys();

			if(rs.next()) {
				new_fid = rs.getInt(1);
			}

			//insert the new file handle into the file data table
			//TODO - make it so that if this insert fails, we can undo the previous insert, too
			query = "INSERT INTO file (fid, data) VALUES (" + new_fid + ", \"\")";
			stmt.executeUpdate(query);
		} catch (SQLException sqle) {
			log.error("Error in createFile():", sqle);
		} finally {
			try { rs.close(); } catch(Exception e) { }
			try { stmt.close(); } catch(Exception e) { }
			try { conn.close(); } catch(Exception e) { }
		}

		//if there was a problem with the insert, then we don't have a new file handle
		if(new_fid < 0) {
			return false;
		}

		/*
		 * This next block of code can be optimized considerably... but for now it's going to work
		 * The general idea is to associate tags with the new file
		 * The problem is that we don't know which tags are new, and which are old
		 * And we don't want to do more inserts than we really have to do...
		 * But for now we're going to do it the lazy way
		 */
		//Insert all of the tags, one at a time
		//any that already exist in the db will fail to insert, but that's okay
		//attempt to make a connection, and insert the file into the attribute table
		Set<String> tags = file.getTags();
		Iterator i = tags.iterator();

		/* for now we will assume that all tags already exist   	    	
    	//loop through all the tags and insert them one at a time
   		while(i.hasNext()) {
   			createTag((String)i.next());
   		}
		 */

		//Now that all the tags are inserted, let's query for them all
		//and for each one we get back, we'll insert it's tag id (tid) into
		//the tag_association table with the new file handle (fid)
		//this could, quite possibly, be some of the ugliest code EVER
		//TODO - make this whole function more efficient!!!
		try {
			i = tags.iterator();
			conn = dataSource.getConnection();
			stmt = conn.createStatement();

			//build a query to get the tag id's for all the tags in our set
			query = "SELECT t.tid FROM tag AS t WHERE ( ";
			while(i.hasNext()) {
				query = query.concat("t.name=\"" + i.next() + "\" OR ");
			}
			query = query.concat("0)");

			//execute the query
			stmt = conn.prepareStatement(query);

			rs = stmt.executeQuery(query);

			//loop through the results of the query and insert each one into the tag association table
			while(rs.next()) {
				query = "INSERT INTO tag_association (fid, tid) VALUES (" + new_fid + ", " + rs.getInt("tid") + ")";
				stmt = conn.createStatement(java.sql.ResultSet.TYPE_FORWARD_ONLY, java.sql.ResultSet.CONCUR_UPDATABLE);
				stmt.executeUpdate(query);
			}
		} catch (SQLException sqle) {
			log.error("Error in createFile():", sqle);
		} finally {
			try { stmt.close(); } catch(Exception e) { }
			try { conn.close(); } catch(Exception e) { }
		}

		//if we get this far, we might as well call it a success!
		return true;
	}

	/**
	 * Delete the given file from the database.
	 * @param file The file info of the file to delete.
	 * @return true if the file was deleted successfully, false otherwise
	 */
	public boolean deleteFile(FifsFile file) {
		//we need to get the file handle so we can use it in the query to delete the file using the correct fid
		Long fid = file.getFh();
		String query = "DELETE file, attribute FROM file, attribute WHERE file.fid=" + fid + " AND attribute.fid=" + fid;
		String query2 = "DELETE tag_association FROM tag_association WHERE tag_association.fid=" + fid;
		Connection conn = null;
		Statement stmt = null;
		Integer num_rows_deleted = 0;

		//connect to db and delete rows
		try {
			conn = dataSource.getConnection();
			stmt = conn.createStatement();
			num_rows_deleted = stmt.executeUpdate(query);
			stmt.executeUpdate(query2);
		} catch (SQLException sqle) {
			log.error("Error deleting file from database:", sqle);
		} finally {
			try { stmt.close(); } catch(Exception e) { }
			try { conn.close(); } catch(Exception e) { }
		}

		if(num_rows_deleted > 0) {
			return true;
		}
		return false;
	}

	/**
	 * Get all files that are tagged with the given set of tags. If the set
	 * of tags is null or contains no tags, then all files in the database
	 * should be returned.
	 * 
	 * @param tags The tags to query on
	 * @return The set of files matching the query
	 */
	public Set<FifsFile> getAllFilesByTags(Set<String> tags) {
		Set<FifsFile> all_files = new HashSet<FifsFile>();
		//go through all the tags and the SQL statement
		String query = "SELECT a.fid FROM attribute AS a";

		if(!tags.isEmpty()) {
			Iterator i = tags.iterator();
			query = query.concat(" JOIN tag_association AS ta ON a.fid=ta.fid JOIN tag AS t ON ta.tid=t.tid");

			query = query.concat(" WHERE");
			while(i.hasNext()) {
				query = query.concat(" t.name = \"" + i.next() + "\" OR");
			}
			query = query.concat(" 0");
		}

		query = query.concat(" GROUP BY a.fid HAVING COUNT(a.fid) >= " + tags.size() + " ORDER BY a.name ASC");

		Connection conn = null;
		PreparedStatement stmt = null;
		ResultSet rs = null;

		//attempt to make a connection, and query the db for all the files with the given set of tags
		try {
			conn = dataSource.getConnection();
			stmt = conn.prepareStatement(query);
			rs = stmt.executeQuery();
			while (rs.next()) {
				//create a new FifsFile and add it to the list of all files
				Long fid = rs.getLong("fid");
				FifsFile new_file = this.getFile(fid);
				all_files.add(new_file);
			}
		} catch (SQLException sqle) {
			log.error("Error getting files by tags:", sqle);
			log.error(query);
		} finally {
			try { rs.close(); } catch(Exception e) { }
			try { stmt.close(); } catch(Exception e) { }
			try { conn.close(); } catch(Exception e) { }
		}

		// otherwise give 'em the set of all files matching the tags specified in the argument
		return all_files;
	}

	/**
	 * Get the file that's tagged with the given set of tags and
	 * with the given filename
	 * 
	 * @param tags The tags of the file to get
	 * @param filename The name of the file to get
	 * @return The FifsFile requested, or null if it doesn't exist
	 */
	public FifsFile getFile(Set<String> tags, String filename){
		FifsFile file_to_return = null;

		//make a connection to the database and see what filehandle is returned when we search
		//for the tags AND filename
		String query = "SELECT a.fid FROM attribute AS a";
		
		//check to see if the set of tags has any tags in it
		if(!tags.isEmpty()) {
			query = query.concat(" JOIN tag_association AS ta ON a.fid=ta.fid JOIN tag AS t ON ta.tid=t.tid WHERE (");
			//if so, create an iterator to walk through the tags
			Iterator i = tags.iterator();

			//we've already started the WHERE clause earlier, now we just need to append it with the tags
			while(i.hasNext()) {
				query = query.concat(" t.name = \"" + i.next() + "\" OR");
			}
			query = query.concat(" 0) AND");
		}
		else {
			query = query.concat(" WHERE");
		}
		query = query.concat(" a.name=\"" + filename + "\"");

		//we need to make sure we only get the file with the unique set of tags
		query = query.concat(" GROUP BY a.fid HAVING COUNT(a.fid)>= " + tags.size());

		Connection conn = null;
		PreparedStatement stmt = null;
		ResultSet rs = null;

		//attempt to make a connection to the db for the file
		try {
			conn = dataSource.getConnection();
			stmt = conn.prepareStatement(query);
			rs = stmt.executeQuery();

			//we don't want to loop through all the options, there *should* only be one
			//NOTE: this is the lazy way to get one and not worry about the other (erroneous) results
			if(rs.next()) {
				Long fid = rs.getLong("fid");
				file_to_return = this.getFile(fid);
			}
		} catch (SQLException sqle) {
			log.error("Error in getFile(tags, filename):", sqle);
			log.error(query);
		} finally {
			try { rs.close(); } catch(Exception e) { }
			try { stmt.close(); } catch(Exception e) { }
			try { conn.close(); } catch(Exception e) { }
		}

		return file_to_return;
	}

	/**
	 * Set the given file's size. The file's data should be truncated or
	 * null expanded as appropriate.
	 * 
	 * @param fh The file handle to update
	 * @param size The new file size
	 * @return true if the file's size was changed, and false otherwise
	 */
	public boolean setFileSize(long fh, long size) {
		//variables for the connection
		Connection conn = null;
		Statement stmt = null;
		PreparedStatement pstmt = null;
		ResultSet rs = null;
		int num_rows_updated = 0;
		int last_block = (int)(size / (long)Fifs.FIFS_BLOCK_SIZE);
		
		//attempt to make a connection, and query the db for the data
		try {
			String query = "SELECT block, data FROM file WHERE fid=" + fh + " ORDER BY block ASC";
			conn = dataSource.getConnection();
			stmt = conn.createStatement();
			rs = stmt.executeQuery(query);

			pstmt = conn.prepareStatement("UPDATE file SET data = ? WHERE fid = " + fh + " AND block = ?");
			int block = 0;
			//if the query returned data
			while(rs.next()  && block != last_block) {
				//get the block and data
				block = rs.getInt("block");
				Blob blob = rs.getBlob("data");

				byte[] current_block = new byte[Fifs.FIFS_BLOCK_SIZE];
				BufferedInputStream in = new BufferedInputStream(blob.getBinaryStream());

				if(block == last_block) {
					for(int i = 0; i < current_block.length; i++) {
						current_block[i] = 0;
					}
					try {
						in.read(current_block, 0, (int)(size % Fifs.FIFS_BLOCK_SIZE));
					} catch (IOException ioe) {
						log.error("Error reading from an input stream:", ioe);
					}

					ByteArrayInputStream inStream = new ByteArrayInputStream(current_block);
					pstmt.setBinaryStream(1,inStream,inStream.available());
					pstmt.setInt(2, block);
					num_rows_updated += pstmt.executeUpdate();
				}
			}

			byte[] current_block = new byte[Fifs.FIFS_BLOCK_SIZE];
			for(int i = 0; i < current_block.length; i++) {
				current_block[i] = 0;
			}
			
			for(int i = block + 1; i <= last_block; i++) {
				ByteArrayInputStream inStream = new ByteArrayInputStream(current_block);
				pstmt.setBinaryStream(1,inStream,inStream.available());
				pstmt.setInt(2, i);
				num_rows_updated += pstmt.executeUpdate();				
			}
			
			//delete any blocks that need to be truly truncated
			query = "DELETE FROM file WHERE fid = " + fh + " AND block > " + last_block;
			num_rows_updated += stmt.executeUpdate(query);
		} catch (SQLException sqle) {
			log.error("Error in setFileSize():", sqle);
		} finally {
			try { rs.close(); } catch(Exception e) { }
			try { pstmt.close(); } catch(Exception e) { }
			try { stmt.close(); } catch(Exception e) { }
			try { conn.close(); } catch(Exception e) { }
		}

		if(num_rows_updated > 0) {
			return true;
		}

		return false;
	}

	/**
	 * Get the file with the given file handle.
	 * 
	 * @param fh The file handle to get
	 * @return The FifsFile requested, or null if it doesn't exist
	 */
	public FifsFile getFile(long fh) {
		FifsFile file_to_return = new FifsFile();
		FuseStat a = new FuseStat();

		Connection conn = null;
		PreparedStatement stmt = null;
		ResultSet rs = null;
		ResultSet rs2 = null;

		String filename = "";
		Integer num_rows = 0;
		Set<String> tags = new HashSet<String>();
		String query = "SELECT a.name AS filename, a.mode, a.ctime, a.mtime, a.atime, a.uid, a.gid, a.size, a.blocks FROM attribute as a WHERE a.fid=" + fh;
		//String query = "SELECT a.name AS filename, a.mode, a.ctime, a.mtime, a.atime, a.uid, a.gid, a.size, a.blocks, t.name AS tag_name FROM attribute as a JOIN tag_association AS ta ON a.fid=ta.fid JOIN tag AS t ON ta.tid=t.tid WHERE a.fid=" + fh;

		//attempt to make a connect, and query the db for the data from
		//the file associated with fh (the file handle).
		try {
			conn = dataSource.getConnection();
			stmt = conn.prepareStatement(query);
			rs = stmt.executeQuery();

			//yet another ugly hack!
			//the idea here is to get the first (and hopefully only) file returned + all the tags for the file at the same time
			if(rs.next()) {
				filename = rs.getString("filename");
				a.mode = rs.getInt("mode");
				a.uid = rs.getInt("uid");
				a.gid = rs.getInt("gid");
				a.ctime = rs.getInt("ctime");
				a.mtime = rs.getInt("mtime");
				a.atime = rs.getInt("atime");
				a.size = rs.getLong("size");
				a.blocks = rs.getInt("blocks");
				num_rows++;

				query = "SELECT t.name as tag_name FROM tag AS t JOIN tag_association AS ta ON t.tid=ta.tid WHERE ta.fid=" + fh;
				stmt = conn.prepareStatement(query);
				rs2 = stmt.executeQuery();
				while(rs2.next()) {
					tags.add(rs2.getString("tag_name"));					
				}
			}

			//as long as at least one row was returned we can set the FifsFile attributes
			if(num_rows > 0) {
				file_to_return.setFh(fh);
				file_to_return.setFilename(filename);
				file_to_return.setTags(tags);
				file_to_return.setAttributes(a);
			}
		} catch (SQLException sqle) {
			log.error("Error in getFile(fh):", sqle);
		} finally {
			try { rs.close(); } catch(Exception e) { }
			try { stmt.close(); } catch(Exception e) { }
			try { conn.close(); } catch(Exception e) { }
		}

		//if we have a file to return, return it.  Otherwise return null
		if(num_rows > 0) {
			return file_to_return;
		}
		return null;
	}

	/**
	 * Update the given file info in the database. Only the given
	 * files's FuseStat object (retrieved with file.getAttributes()) or its
	 * filename may have changed.
	 * 
	 * @param file The new file info as it should appear in the database
	 * @return true if the update worked, false otherwise
	 */
	public boolean updateFileAttributes(FifsFile file) {    	
		FuseStat a = file.getAttributes();
		String query = "UPDATE attribute SET name = \"" + file.getFilename() + 
		"\", uid = " + a.uid + ", gid = " + a.gid + ", mode = " + a.mode + 
		", ctime = " + a.ctime + 
		", mtime = " + a.mtime + 
		", atime = " + a.atime + 
		", size = " + a.size +
		", blocks = " + a.blocks +
		" WHERE fid = " + file.getFh();
		Connection conn = null;
		Statement stmt = null;
		int num_rows_updated = 0;

		//attempt to make a connection, and then update the file's attributes
		try {
			conn = dataSource.getConnection();
			stmt = conn.prepareStatement(query);

			num_rows_updated = stmt.executeUpdate(query);
		} catch (SQLException sqle) {
			log.error("Error in updateFileAttributes(file):", sqle);
		} finally {
			try { stmt.close(); } catch(Exception e) { }
			try { conn.close(); } catch(Exception e) { }
		}

		//if the update worked, return true
		if(num_rows_updated > 0) {
			return true;
		}

		return false;
	}

	/**
	 * Update the given file's tags in the database. Only the given
	 * files's tag set (retrieved with file.getTags()) may have
	 * changed. Tags may have been removed or added.
	 * 
	 * @param file The new file info as it should appear in the database
	 * @return true if the update worked, false otherwise
	 */
	public boolean updateFileTags(FifsFile file) {
		Connection conn = null;
		Statement stmt = null;
		ResultSet rs = null;

		Set<String> tags = file.getTags();
		Iterator i = tags.iterator();

		//TODO - implement this method more efficiently
		//delete all of the associations of the file out of tag_association
		//find all the tag id's in the tag file
		//insert into tag_association
		try {
			i = tags.iterator();
			conn = dataSource.getConnection();
			stmt = conn.createStatement();

			//create and execute query to delete all of the relevant tag associations
			String query = "DELETE FROM tag_association WHERE fid=" + file.getFh();
			stmt.execute(query);

			//build a query to get the tag id's for all the tags in our set
			query = "SELECT t.tid FROM tag AS t WHERE ( ";
			while(i.hasNext()) {
				query = query.concat("t.name=\"" + i.next() + "\" OR ");
			}
			query = query.concat("0)");

			//execute the query
			stmt = conn.prepareStatement(query);        	
			rs = stmt.executeQuery(query);

			//loop through the results of the query and insert each one into the tag association table
			while(rs.next()) {
				query = "INSERT INTO tag_association (fid, tid) VALUES (" + file.getFh() + ", " + rs.getInt("tid") + ")";
				stmt = conn.createStatement(java.sql.ResultSet.TYPE_FORWARD_ONLY, java.sql.ResultSet.CONCUR_UPDATABLE);
				stmt.executeUpdate(query);
			}
		} catch (SQLException sqle) {
			log.error("Error in updateFileTags(file):", sqle);
		} finally {
			try { rs.close(); } catch(Exception e) { }
			try { stmt.close(); } catch(Exception e) { }
			try { conn.close(); } catch(Exception e) { }
		}

		//TODO - make it so we aren't always returning true
		return true;
	}

	/**
	 * Get the bytes in the given file
	 * 
	 * @param fh The filehandle to get
	 * @param offset The offset within the file to start reading
	 * @param length The maximum number of bytes to return in the array
	 * @return an array of bytes for the given file handle, or null if
	 * it cannot be found. The length of the array should reflect the number
	 * of bytes returned, not the length requested. An array of length zero
	 * indicates end of file.
	 */
	public byte[] getFileBytes(long fh, long offset, long length) {
		//variables for the connection
		Connection conn = null;
		PreparedStatement stmt = null;
		ResultSet rs = null;

		//TODO - figure out what to do if the requested length is bigger than an int
		byte[] bytes = new byte[(int)length];
		int num_bytes_read = 0;
		
		int first_block = (int)((offset / (long)Fifs.FIFS_BLOCK_SIZE) + 1);
		int last_block = (int)(((offset + length) / (long)Fifs.FIFS_BLOCK_SIZE) + 1);
		int offset_into_start_block = (int)(offset % (long)Fifs.FIFS_BLOCK_SIZE);

		//attempt to make a connection, and query the db for the data
		try {
			//query to get the data from the database
			String query = "SELECT block, data FROM file WHERE fid=" + fh + " AND block>=" + first_block + " AND block<=" + last_block + " ORDER BY block ASC";
			conn = dataSource.getConnection();
			stmt = conn.prepareStatement(query);
			rs = stmt.executeQuery();

			//if the query returned data
			while(rs.next()) {
				//current block number
				int block = rs.getInt("block");
				
				//blob data
				Blob blob = rs.getBlob("data");
				BufferedInputStream in = new BufferedInputStream(blob.getBinaryStream());

				try {
					//if this is the first block, skip the offset into the first block
					if(block == first_block) {
						in.skip(offset_into_start_block);
						
						//we only want to read as many bytes as we need to read
						//so figure out which is smaller: the bytes in b or the block size
						int max_bytes_to_read = Fifs.FIFS_BLOCK_SIZE - offset_into_start_block;
						if(bytes.length < Fifs.FIFS_BLOCK_SIZE - offset_into_start_block) {
							max_bytes_to_read = bytes.length;
						}
						
						//and then read the data from the blob
						in.read(bytes, num_bytes_read, max_bytes_to_read);
						num_bytes_read += max_bytes_to_read;
						
						//count how many bytes we actually read
						//for(int i = num_bytes_read; i < Fifs.FIFS_BLOCK_SIZE && i < bytes.length; i++) {
							//if the current byte is NOT -1, count the byte as read from the stream
							//if(bytes[i] != -1) { num_bytes_read++; }
						//}
					} else {
						//figure out how many bytes to read
						int max_bytes_to_read = Fifs.FIFS_BLOCK_SIZE;
						if(bytes.length - num_bytes_read < Fifs.FIFS_BLOCK_SIZE) {
							max_bytes_to_read = bytes.length - num_bytes_read;
						}
						
						in.read(bytes, num_bytes_read, max_bytes_to_read);
						num_bytes_read += max_bytes_to_read;
					}
				} catch (IOException ioe) {
					log.error("Error reading from an input stream:", ioe);
				}
			}
		} catch (SQLException sqle) {
			log.error("Error reading data from database:", sqle);
		} finally {
			try { rs.close(); } catch(Exception e) { }
			try { stmt.close(); } catch(Exception e) { }
			try { conn.close(); } catch(Exception e) { }
		}

		//if we're this far and no not a single byte has been read, return null
		if(num_bytes_read == 0) {
			return null;
		}

		//since the array's length needs to reflect the number of bytes read
		//and not the number of bytes requested, we need
		//to do some array resizing and copying.  BUT only if necessary
		if(num_bytes_read == bytes.length) {
			return bytes;
		}
		
		byte[] array_to_return = new byte[(int)num_bytes_read];
		for(int i = 0; i < num_bytes_read; i++) {
			array_to_return[i] = bytes[i];
		}

		return array_to_return;
	}

	/**
	 * Write the given bytes into the file at the given offset
	 * 
	 * @param fh The filehandle to write
	 * @param offset The offset within the file to start the write at
	 * @param bytes The bytes to write
	 * @return true if the write was successful, false otherwise
	 */
	public boolean writeFileBytes(long fh, long offset, byte[] bytes) {
		//variables for the DB connection
		Connection conn = null;
		Statement stmt = null;
		PreparedStatement pstmt = null;
		ResultSet rs = null;
		int num_rows_updated = 0;

		int first_block = (int)((offset / (long)Fifs.FIFS_BLOCK_SIZE) + 1);
		int last_block = (int)(((offset + bytes.length) / (long)Fifs.FIFS_BLOCK_SIZE) + 1);
		int offset_into_first_block = (int)(offset % Fifs.FIFS_BLOCK_SIZE);
		
		//attempt to make a connection, and query the db for the data
		try {
			String query = "SELECT block, data FROM file AS f WHERE f.fid=" + fh + " AND block>=" + first_block + " AND block <=" + last_block + " ORDER BY block ASC";
			conn = dataSource.getConnection();
			stmt = conn.createStatement();
			rs = stmt.executeQuery(query);

			int block = 0;
			int bytes_written = 0;
			byte[] current_block = new byte[Fifs.FIFS_BLOCK_SIZE];

			//loop through all the blocks
			while(rs.next()) {
				//get the blob data and the total size of the file
				Blob blob = rs.getBlob("data");				
				block = rs.getInt("block");
				BufferedInputStream in = new BufferedInputStream(blob.getBinaryStream());

				//null out the current block
				for(int i=0; i < current_block.length; i++) {
					current_block[i] = 0;
				}
				
				//read the blob data into the current block
				try {
					in.read(current_block);
				} catch (IOException ioe) {
					log.error("Error reading from an input stream:", ioe);
				}

				//overwrite the data that already exists in the blob (or the null expansion) with the data
				//that was passed to this method
				if(block == first_block) {
					int max_bytes_to_copy = Fifs.FIFS_BLOCK_SIZE - offset_into_first_block;
					if(max_bytes_to_copy > bytes.length - bytes_written) {
						max_bytes_to_copy = bytes.length - bytes_written;
					}
					System.arraycopy(bytes, bytes_written, current_block, offset_into_first_block, max_bytes_to_copy);
					bytes_written += max_bytes_to_copy;
				} else {
					int max_bytes_to_copy = current_block.length;
					if(max_bytes_to_copy > bytes.length - bytes_written) {
						max_bytes_to_copy = bytes.length - bytes_written;
					}
					System.arraycopy(bytes, bytes_written, current_block, 0, max_bytes_to_copy);
					bytes_written += max_bytes_to_copy;
				}

				//create the binary input stream, prepared statement and assign the arguments for the prepared statement
				pstmt = conn.prepareStatement("UPDATE file SET data = ? WHERE fid = " + fh + " AND block = ?");
				ByteArrayInputStream inStream = new ByteArrayInputStream(current_block);
				pstmt.setBinaryStream(1,inStream,inStream.available());
				pstmt.setInt(2, block);

				//execute the update and record how many rows were updated
				num_rows_updated += pstmt.executeUpdate();
			}
			
			pstmt = conn.prepareStatement("INSERT INTO file (fid, data) VALUES(" + fh + ", ?)");
			for(int i = block + 1; i <= last_block; i++) {
				//null out the current block
				for(int j=0; j < current_block.length; j++) {
					current_block[j] = 0;
				}
				
				//copy the bytes to be written into the current block
				int max_bytes_to_copy = current_block.length;
				if(max_bytes_to_copy > bytes.length - bytes_written) {
					max_bytes_to_copy = bytes.length - bytes_written;
				}
				System.arraycopy(bytes, bytes_written, current_block, 0, max_bytes_to_copy);
				bytes_written += max_bytes_to_copy;

				//create the binary input stream and assign the arguments for the prepared statement
				ByteArrayInputStream inStream = new ByteArrayInputStream(current_block);
				pstmt.setBinaryStream(1,inStream,inStream.available());

				//execute the insert and record how many rows were inserted
				num_rows_updated += pstmt.executeUpdate();
			}
		} catch (SQLException sqle) {
			log.error("Error in writeFileBytes():", sqle);
		} finally {
			try { rs.close(); } catch(Exception e) { }
			try { pstmt.close(); } catch(Exception e) { }
			try { stmt.close(); } catch(Exception e) { }
			try { conn.close(); } catch(Exception e) { }
		}

		//as long as we updated at least one record and there was data to be written
		//return true
		if(num_rows_updated > 0 && bytes.length > 0) {
			return true;
		}

		return false;
	}
}

